package com.alex.taskmanager.dao.jdbc;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Collection;

import javax.sql.DataSource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alex.taskmanager.dao.TaskData;
import com.alex.taskmanager.dao.TaskManagerDao;
import com.alex.taskmanager.dao.TaskStatus;
import com.alex.taskmanager.serializer.Serializer;
import com.alex.taskmanager.serializer.SerializerException;
import com.alex.taskmanager.service.TaskExecutor;
import com.alex.taskmanager.service.TaskManagerException;

public class JdbcTaskManagerDao implements TaskManagerDao {

	@SuppressWarnings("unused")
	private static final Logger LOGGER = LoggerFactory.getLogger(JdbcTaskManagerDao.class);

	private static final String INSERT_TASK_QUERY = 
			"INSERT INTO task_data (name, status, executionTime, retryNumber, maxRetries, executor, parameters) VALUES (?, ?, ?, ?, ?, ?, ?)";

	private static final String SELECT_TASKS_QUERY = 
			"SELECT * FROM task_data WHERE status='" + TaskStatus.PENDING + "' AND executionTime<=? LIMIT ? FOR UPDATE";
	
	private static final String UPDATE_SELECTED_TASKS_QUERY = 
			"UPDATE task_data SET status='" + TaskStatus.IN_PROGRESS + "' WHERE id=?";
	
	private static final String UPDATE_TASK_QUERY = 
			"UPDATE task_data SET status=?, executionTime=?, retryNumber=? WHERE id=?";
	
	private static final String DELETE_TASK_QUERY = 
			"DELETE FROM task_data WHERE id=?";

	private DataSource dataSource;

	private Serializer serializer;

	public JdbcTaskManagerDao() {
	}
	
	public JdbcTaskManagerDao(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public void setSerializer(Serializer serializer) {
		this.serializer = serializer;
	}

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	public <ParamType> void storeTask(TaskData<ParamType> taskData) throws TaskManagerException {
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = dataSource.getConnection();
			ps = connection.prepareStatement(INSERT_TASK_QUERY, Statement.RETURN_GENERATED_KEYS);
			int n = 1;
			ps.setString(n++, taskData.getName());
			ps.setString(n++, taskData.getStatus().toString());
			ps.setInt(n++, taskData.getExecutionTime());
			ps.setInt(n++, taskData.getRetryNumber());
			ps.setInt(n++, taskData.getMaxRetries());
			ps.setString(n++, serializer.serialize(taskData.getExecutor()));
			ps.setString(n++, serializer.serialize(taskData.getParameters()));
			if (ps.executeUpdate() != 1) {
				throw new TaskManagerException("Failed to insert task \"" + taskData.getName() + "\"");
			}
			taskData.setId(JdbcUtils.getGeneratedKey(ps));
		} catch (SQLException e) {
			throw new TaskManagerException(e);
		} finally {
			JdbcUtils.closePreparedStatement(ps);
			JdbcUtils.closeConnection(connection);
		}
	}

	public <ParamType> Collection<TaskData<ParamType>> getTasks(int maxCount, int time) throws TaskManagerException {
		Connection connection = null;
		ResultSet resultSet = null;
		PreparedStatement selectPreparedStatement = null;
		PreparedStatement updatePreparedStatement = null;
		try {
			connection = dataSource.getConnection();
			selectPreparedStatement = connection.prepareStatement(SELECT_TASKS_QUERY);
			selectPreparedStatement.setInt(1, time);
			selectPreparedStatement.setInt(2, maxCount);
			resultSet = selectPreparedStatement.executeQuery();

			updatePreparedStatement = connection.prepareStatement(UPDATE_SELECTED_TASKS_QUERY);
			
			Collection<TaskData<ParamType>> tasks = new ArrayList<TaskData<ParamType>>();
			while (resultSet.next()) {
				TaskData<ParamType> taskData = mapTask(resultSet);
				tasks.add(taskData);
				updatePreparedStatement.setInt(1, taskData.getId());
				updatePreparedStatement.addBatch();
			}
			
			int[] result = updatePreparedStatement.executeBatch();
			for (int res : result) {
				if (res != 1) {
					throw new TaskManagerException("Failed to update task status");
				}
			}
			
			return tasks;
		} catch (SQLException e) {
			throw new TaskManagerException(e);
		} catch (SerializerException e) {
			throw new TaskManagerException(e);
		} finally {
			JdbcUtils.closeResultSet(resultSet);
			JdbcUtils.closePreparedStatement(selectPreparedStatement);
			JdbcUtils.closePreparedStatement(updatePreparedStatement);
			JdbcUtils.closeConnection(connection);
		}
	}
	
	public <ParamType> void updateTask(TaskData<ParamType> taskData) throws TaskManagerException {
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = dataSource.getConnection();
			ps = connection.prepareStatement(UPDATE_TASK_QUERY);
			int n = 1;
			ps.setString(n++, taskData.getStatus().toString());
			ps.setInt(n++, taskData.getExecutionTime());
			ps.setInt(n++, taskData.getRetryNumber());
			if (ps.executeUpdate() != 1) {
				throw new TaskManagerException("Failed to update task [" + taskData.getId() + "]");
			}
		} catch (SQLException e) {
			throw new TaskManagerException(e);
		} finally {
			JdbcUtils.closePreparedStatement(ps);
			JdbcUtils.closeConnection(connection);
		}
	}
	
	public <ParamType> void deleteTask(TaskData<ParamType> taskData) throws TaskManagerException {
		Connection connection = null;
		PreparedStatement ps = null;
		try {
			connection = dataSource.getConnection();
			ps = connection.prepareStatement(DELETE_TASK_QUERY);
			ps.setInt(1, taskData.getId());
			if (ps.executeUpdate() != 1) {
				throw new TaskManagerException("Failed to delete task [" + taskData.getId() + "]");
			}
		} catch (SQLException e) {
			throw new TaskManagerException(e);
		} finally {
			JdbcUtils.closePreparedStatement(ps);
			JdbcUtils.closeConnection(connection);
		}
	}

	@SuppressWarnings("unchecked")
	private <ParamType> TaskData<ParamType> mapTask(ResultSet resultSet) throws SQLException, SerializerException {
		TaskData<ParamType> taskData = new TaskData<ParamType>();
		taskData.setId(resultSet.getInt("id"));
		taskData.setName(resultSet.getString("name"));
		taskData.setRetryNumber(resultSet.getInt("retryNumber"));
		taskData.setMaxRetries(resultSet.getInt("maxRetries"));
		taskData.setStatus(TaskStatus.valueOf(resultSet.getString("status")));
		taskData.setExecutor((TaskExecutor<ParamType>) serializer.deserialize(resultSet.getString("executor")));
		taskData.setParameters((ParamType) serializer.deserialize(resultSet.getString("parameters")));
		return taskData;
	}
}
